// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('../../lib/codemirror'));
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode('verilog', function(config, parserConfig) {
    var indentUnit = config.indentUnit,
      statementIndentUnit = parserConfig.statementIndentUnit || indentUnit,
      dontAlignCalls = parserConfig.dontAlignCalls,
      noIndentKeywords = parserConfig.noIndentKeywords || [],
      multiLineStrings = parserConfig.multiLineStrings,
      hooks = parserConfig.hooks || {};

    function words(str) {
      var obj = {},
        words = str.split(' ');
      for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
      return obj;
    }

    /**
     * Keywords from IEEE 1800-2012
     */
    var keywords = words(
      'accept_on alias always always_comb always_ff always_latch and assert assign assume automatic before begin bind ' +
        'bins binsof bit break buf bufif0 bufif1 byte case casex casez cell chandle checker class clocking cmos config ' +
        'const constraint context continue cover covergroup coverpoint cross deassign default defparam design disable ' +
        'dist do edge else end endcase endchecker endclass endclocking endconfig endfunction endgenerate endgroup ' +
        'endinterface endmodule endpackage endprimitive endprogram endproperty endspecify endsequence endtable endtask ' +
        'enum event eventually expect export extends extern final first_match for force foreach forever fork forkjoin ' +
        'function generate genvar global highz0 highz1 if iff ifnone ignore_bins illegal_bins implements implies import ' +
        'incdir include initial inout input inside instance int integer interconnect interface intersect join join_any ' +
        'join_none large let liblist library local localparam logic longint macromodule matches medium modport module ' +
        'nand negedge nettype new nexttime nmos nor noshowcancelled not notif0 notif1 null or output package packed ' +
        'parameter pmos posedge primitive priority program property protected pull0 pull1 pulldown pullup ' +
        'pulsestyle_ondetect pulsestyle_onevent pure rand randc randcase randsequence rcmos real realtime ref reg ' +
        'reject_on release repeat restrict return rnmos rpmos rtran rtranif0 rtranif1 s_always s_eventually s_nexttime ' +
        's_until s_until_with scalared sequence shortint shortreal showcancelled signed small soft solve specify ' +
        'specparam static string strong strong0 strong1 struct super supply0 supply1 sync_accept_on sync_reject_on ' +
        'table tagged task this throughout time timeprecision timeunit tran tranif0 tranif1 tri tri0 tri1 triand trior ' +
        'trireg type typedef union unique unique0 unsigned until until_with untyped use uwire var vectored virtual void ' +
        'wait wait_order wand weak weak0 weak1 while wildcard wire with within wor xnor xor',
    );

    /** Operators from IEEE 1800-2012
     unary_operator ::=
       + | - | ! | ~ | & | ~& | | | ~| | ^ | ~^ | ^~
     binary_operator ::=
       + | - | * | / | % | == | != | === | !== | ==? | !=? | && | || | **
       | < | <= | > | >= | & | | | ^ | ^~ | ~^ | >> | << | >>> | <<<
       | -> | <->
     inc_or_dec_operator ::= ++ | --
     unary_module_path_operator ::=
       ! | ~ | & | ~& | | | ~| | ^ | ~^ | ^~
     binary_module_path_operator ::=
       == | != | && | || | & | | | ^ | ^~ | ~^
  */
    var isOperatorChar = /[\+\-\*\/!~&|^%=?:]/;
    var isBracketChar = /[\[\]{}()]/;

    var unsignedNumber = /\d[0-9_]*/;
    var decimalLiteral = /\d*\s*'s?d\s*\d[0-9_]*/i;
    var binaryLiteral = /\d*\s*'s?b\s*[xz01][xz01_]*/i;
    var octLiteral = /\d*\s*'s?o\s*[xz0-7][xz0-7_]*/i;
    var hexLiteral = /\d*\s*'s?h\s*[0-9a-fxz?][0-9a-fxz?_]*/i;
    var realLiteral = /(\d[\d_]*(\.\d[\d_]*)?E-?[\d_]+)|(\d[\d_]*\.\d[\d_]*)/i;

    var closingBracketOrWord = /^((\w+)|[)}\]])/;
    var closingBracket = /[)}\]]/;

    var curPunc;
    var curKeyword;

    // Block openings which are closed by a matching keyword in the form of ("end" + keyword)
    // E.g. "task" => "endtask"
    var blockKeywords = words(
      'case checker class clocking config function generate interface module package ' +
        'primitive program property specify sequence table task',
    );

    // Opening/closing pairs
    var openClose = {};
    for (var keyword in blockKeywords) {
      openClose[keyword] = 'end' + keyword;
    }
    openClose['begin'] = 'end';
    openClose['casex'] = 'endcase';
    openClose['casez'] = 'endcase';
    openClose['do'] = 'while';
    openClose['fork'] = 'join;join_any;join_none';
    openClose['covergroup'] = 'endgroup';

    for (var i in noIndentKeywords) {
      var keyword = noIndentKeywords[i];
      if (openClose[keyword]) {
        openClose[keyword] = undefined;
      }
    }

    // Keywords which open statements that are ended with a semi-colon
    var statementKeywords = words(
      'always always_comb always_ff always_latch assert assign assume else export for foreach forever if import initial repeat while',
    );

    function tokenBase(stream, state) {
      var ch = stream.peek(),
        style;
      if (hooks[ch] && (style = hooks[ch](stream, state)) != false)
        return style;
      if (hooks.tokenBase && (style = hooks.tokenBase(stream, state)) != false)
        return style;

      if (/[,;:\.]/.test(ch)) {
        curPunc = stream.next();
        return null;
      }
      if (isBracketChar.test(ch)) {
        curPunc = stream.next();
        return 'bracket';
      }
      // Macros (tick-defines)
      if (ch == '`') {
        stream.next();
        if (stream.eatWhile(/[\w\$_]/)) {
          return 'def';
        } else {
          return null;
        }
      }
      // System calls
      if (ch == '$') {
        stream.next();
        if (stream.eatWhile(/[\w\$_]/)) {
          return 'meta';
        } else {
          return null;
        }
      }
      // Time literals
      if (ch == '#') {
        stream.next();
        stream.eatWhile(/[\d_.]/);
        return 'def';
      }
      // Strings
      if (ch == '"') {
        stream.next();
        state.tokenize = tokenString(ch);
        return state.tokenize(stream, state);
      }
      // Comments
      if (ch == '/') {
        stream.next();
        if (stream.eat('*')) {
          state.tokenize = tokenComment;
          return tokenComment(stream, state);
        }
        if (stream.eat('/')) {
          stream.skipToEnd();
          return 'comment';
        }
        stream.backUp(1);
      }

      // Numeric literals
      if (
        stream.match(realLiteral) ||
        stream.match(decimalLiteral) ||
        stream.match(binaryLiteral) ||
        stream.match(octLiteral) ||
        stream.match(hexLiteral) ||
        stream.match(unsignedNumber) ||
        stream.match(realLiteral)
      ) {
        return 'number';
      }

      // Operators
      if (stream.eatWhile(isOperatorChar)) {
        return 'meta';
      }

      // Keywords / plain variables
      if (stream.eatWhile(/[\w\$_]/)) {
        var cur = stream.current();
        if (keywords[cur]) {
          if (openClose[cur]) {
            curPunc = 'newblock';
          }
          if (statementKeywords[cur]) {
            curPunc = 'newstatement';
          }
          curKeyword = cur;
          return 'keyword';
        }
        return 'variable';
      }

      stream.next();
      return null;
    }

    function tokenString(quote) {
      return function(stream, state) {
        var escaped = false,
          next,
          end = false;
        while ((next = stream.next()) != null) {
          if (next == quote && !escaped) {
            end = true;
            break;
          }
          escaped = !escaped && next == '\\';
        }
        if (end || !(escaped || multiLineStrings)) state.tokenize = tokenBase;
        return 'string';
      };
    }

    function tokenComment(stream, state) {
      var maybeEnd = false,
        ch;
      while ((ch = stream.next())) {
        if (ch == '/' && maybeEnd) {
          state.tokenize = tokenBase;
          break;
        }
        maybeEnd = ch == '*';
      }
      return 'comment';
    }

    function Context(indented, column, type, align, prev) {
      this.indented = indented;
      this.column = column;
      this.type = type;
      this.align = align;
      this.prev = prev;
    }
    function pushContext(state, col, type) {
      var indent = state.indented;
      var c = new Context(indent, col, type, null, state.context);
      return (state.context = c);
    }
    function popContext(state) {
      var t = state.context.type;
      if (t == ')' || t == ']' || t == '}') {
        state.indented = state.context.indented;
      }
      return (state.context = state.context.prev);
    }

    function isClosing(text, contextClosing) {
      if (text == contextClosing) {
        return true;
      } else {
        // contextClosing may be multiple keywords separated by ;
        var closingKeywords = contextClosing.split(';');
        for (var i in closingKeywords) {
          if (text == closingKeywords[i]) {
            return true;
          }
        }
        return false;
      }
    }

    function buildElectricInputRegEx() {
      // Reindentation should occur on any bracket char: {}()[]
      // or on a match of any of the block closing keywords, at
      // the end of a line
      var allClosings = [];
      for (var i in openClose) {
        if (openClose[i]) {
          var closings = openClose[i].split(';');
          for (var j in closings) {
            allClosings.push(closings[j]);
          }
        }
      }
      var re = new RegExp('[{}()\\[\\]]|(' + allClosings.join('|') + ')$');
      return re;
    }

    // Interface
    return {
      // Regex to force current line to reindent
      electricInput: buildElectricInputRegEx(),

      startState: function(basecolumn) {
        var state = {
          tokenize: null,
          context: new Context((basecolumn || 0) - indentUnit, 0, 'top', false),
          indented: 0,
          startOfLine: true,
        };
        if (hooks.startState) hooks.startState(state);
        return state;
      },

      token: function(stream, state) {
        var ctx = state.context;
        if (stream.sol()) {
          if (ctx.align == null) ctx.align = false;
          state.indented = stream.indentation();
          state.startOfLine = true;
        }
        if (hooks.token) {
          // Call hook, with an optional return value of a style to override verilog styling.
          var style = hooks.token(stream, state);
          if (style !== undefined) {
            return style;
          }
        }
        if (stream.eatSpace()) return null;
        curPunc = null;
        curKeyword = null;
        var style = (state.tokenize || tokenBase)(stream, state);
        if (style == 'comment' || style == 'meta' || style == 'variable')
          return style;
        if (ctx.align == null) ctx.align = true;

        if (curPunc == ctx.type) {
          popContext(state);
        } else if (
          (curPunc == ';' && ctx.type == 'statement') ||
          (ctx.type && isClosing(curKeyword, ctx.type))
        ) {
          ctx = popContext(state);
          while (ctx && ctx.type == 'statement') ctx = popContext(state);
        } else if (curPunc == '{') {
          pushContext(state, stream.column(), '}');
        } else if (curPunc == '[') {
          pushContext(state, stream.column(), ']');
        } else if (curPunc == '(') {
          pushContext(state, stream.column(), ')');
        } else if (ctx && ctx.type == 'endcase' && curPunc == ':') {
          pushContext(state, stream.column(), 'statement');
        } else if (curPunc == 'newstatement') {
          pushContext(state, stream.column(), 'statement');
        } else if (curPunc == 'newblock') {
          if (
            curKeyword == 'function' &&
            ctx &&
            (ctx.type == 'statement' || ctx.type == 'endgroup')
          ) {
            // The 'function' keyword can appear in some other contexts where it actually does not
            // indicate a function (import/export DPI and covergroup definitions).
            // Do nothing in this case
          } else if (curKeyword == 'task' && ctx && ctx.type == 'statement') {
            // Same thing for task
          } else {
            var close = openClose[curKeyword];
            pushContext(state, stream.column(), close);
          }
        }

        state.startOfLine = false;
        return style;
      },

      indent: function(state, textAfter) {
        if (state.tokenize != tokenBase && state.tokenize != null)
          return CodeMirror.Pass;
        if (hooks.indent) {
          var fromHook = hooks.indent(state);
          if (fromHook >= 0) return fromHook;
        }
        var ctx = state.context,
          firstChar = textAfter && textAfter.charAt(0);
        if (ctx.type == 'statement' && firstChar == '}') ctx = ctx.prev;
        var closing = false;
        var possibleClosing = textAfter.match(closingBracketOrWord);
        if (possibleClosing) closing = isClosing(possibleClosing[0], ctx.type);
        if (ctx.type == 'statement')
          return ctx.indented + (firstChar == '{' ? 0 : statementIndentUnit);
        else if (closingBracket.test(ctx.type) && ctx.align && !dontAlignCalls)
          return ctx.column + (closing ? 0 : 1);
        else if (ctx.type == ')' && !closing)
          return ctx.indented + statementIndentUnit;
        else return ctx.indented + (closing ? 0 : indentUnit);
      },

      blockCommentStart: '/*',
      blockCommentEnd: '*/',
      lineComment: '//',
    };
  });

  CodeMirror.defineMIME('text/x-verilog', {
    name: 'verilog',
  });

  CodeMirror.defineMIME('text/x-systemverilog', {
    name: 'verilog',
  });

  // TL-Verilog mode.
  // See tl-x.org for language spec.
  // See the mode in action at makerchip.com.
  // Contact: steve.hoover@redwoodeda.com

  // TLV Identifier prefixes.
  // Note that sign is not treated separately, so "+/-" versions of numeric identifiers
  // are included.
  var tlvIdentifierStyle = {
    '|': 'link',
    '>': 'property', // Should condition this off for > TLV 1c.
    $: 'variable',
    $$: 'variable',
    '?$': 'qualifier',
    '?*': 'qualifier',
    '-': 'hr',
    '/': 'property',
    '/-': 'property',
    '@': 'variable-3',
    '@-': 'variable-3',
    '@++': 'variable-3',
    '@+=': 'variable-3',
    '@+=-': 'variable-3',
    '@--': 'variable-3',
    '@-=': 'variable-3',
    '%+': 'tag',
    '%-': 'tag',
    '%': 'tag',
    '>>': 'tag',
    '<<': 'tag',
    '<>': 'tag',
    '#': 'tag', // Need to choose a style for this.
    '^': 'attribute',
    '^^': 'attribute',
    '^!': 'attribute',
    '*': 'variable-2',
    '**': 'variable-2',
    '\\': 'keyword',
    '"': 'comment',
  };

  // Lines starting with these characters define scope (result in indentation).
  var tlvScopePrefixChars = {
    '/': 'beh-hier',
    '>': 'beh-hier',
    '-': 'phys-hier',
    '|': 'pipe',
    '?': 'when',
    '@': 'stage',
    '\\': 'keyword',
  };
  var tlvIndentUnit = 3;
  var tlvTrackStatements = false;
  var tlvIdentMatch = /^([~!@#\$%\^&\*-\+=\?\/\\\|'"<>]+)([\d\w_]*)/; // Matches an identifiere.
  // Note that ':' is excluded, because of it's use in [:].
  var tlvFirstLevelIndentMatch = /^[! ]  /;
  var tlvLineIndentationMatch = /^[! ] */;
  var tlvCommentMatch = /^\/[\/\*]/;

  // Returns a style specific to the scope at the given indentation column.
  // Type is one of: "indent", "scope-ident", "before-scope-ident".
  function tlvScopeStyle(state, indentation, type) {
    // Begin scope.
    var depth = indentation / tlvIndentUnit; // TODO: Pass this in instead.
    return 'tlv-' + state.tlvIndentationStyle[depth] + '-' + type;
  }

  // Return true if the next thing in the stream is an identifier with a mnemonic.
  function tlvIdentNext(stream) {
    var match;
    return (match = stream.match(tlvIdentMatch, false)) && match[2].length > 0;
  }

  CodeMirror.defineMIME('text/x-tlv', {
    name: 'verilog',

    hooks: {
      electricInput: false,

      // Return undefined for verilog tokenizing, or style for TLV token (null not used).
      // Standard CM styles are used for most formatting, but some TL-Verilog-specific highlighting
      // can be enabled with the definition of cm-tlv-* styles, including highlighting for:
      //   - M4 tokens
      //   - TLV scope indentation
      //   - Statement delimitation (enabled by tlvTrackStatements)
      token: function(stream, state) {
        var style = undefined;
        var match; // Return value of pattern matches.

        // Set highlighting mode based on code region (TLV or SV).
        if (stream.sol() && !state.tlvInBlockComment) {
          // Process region.
          if (stream.peek() == '\\') {
            style = 'def';
            stream.skipToEnd();
            if (stream.string.match(/\\SV/)) {
              state.tlvCodeActive = false;
            } else if (stream.string.match(/\\TLV/)) {
              state.tlvCodeActive = true;
            }
          }
          // Correct indentation in the face of a line prefix char.
          if (
            state.tlvCodeActive &&
            stream.pos == 0 &&
            state.indented == 0 &&
            (match = stream.match(tlvLineIndentationMatch, false))
          ) {
            state.indented = match[0].length;
          }

          // Compute indentation state:
          //   o Auto indentation on next line
          //   o Indentation scope styles
          var indented = state.indented;
          var depth = indented / tlvIndentUnit;
          if (depth <= state.tlvIndentationStyle.length) {
            // not deeper than current scope

            var blankline = stream.string.length == indented;
            var chPos = depth * tlvIndentUnit;
            if (chPos < stream.string.length) {
              var bodyString = stream.string.slice(chPos);
              var ch = bodyString[0];
              if (
                tlvScopePrefixChars[ch] &&
                (match = bodyString.match(tlvIdentMatch)) &&
                tlvIdentifierStyle[match[1]]
              ) {
                // This line begins scope.
                // Next line gets indented one level.
                indented += tlvIndentUnit;
                // Style the next level of indentation (except non-region keyword identifiers,
                //   which are statements themselves)
                if (!(ch == '\\' && chPos > 0)) {
                  state.tlvIndentationStyle[depth] = tlvScopePrefixChars[ch];
                  if (tlvTrackStatements) {
                    state.statementComment = false;
                  }
                  depth++;
                }
              }
            }
            // Clear out deeper indentation levels unless line is blank.
            if (!blankline) {
              while (state.tlvIndentationStyle.length > depth) {
                state.tlvIndentationStyle.pop();
              }
            }
          }
          // Set next level of indentation.
          state.tlvNextIndent = indented;
        }

        if (state.tlvCodeActive) {
          // Highlight as TLV.

          var beginStatement = false;
          if (tlvTrackStatements) {
            // This starts a statement if the position is at the scope level
            // and we're not within a statement leading comment.
            beginStatement =
              stream.peek() != ' ' && // not a space
              style === undefined && // not a region identifier
              !state.tlvInBlockComment && // not in block comment
              //!stream.match(tlvCommentMatch, false) && // not comment start
              stream.column() ==
                state.tlvIndentationStyle.length * tlvIndentUnit; // at scope level
            if (beginStatement) {
              if (state.statementComment) {
                // statement already started by comment
                beginStatement = false;
              }
              state.statementComment = stream.match(tlvCommentMatch, false); // comment start
            }
          }

          var match;
          if (style !== undefined) {
            // Region line.
            style += ' ' + tlvScopeStyle(state, 0, 'scope-ident');
          } else if (
            stream.pos / tlvIndentUnit < state.tlvIndentationStyle.length &&
            (match = stream.match(
              stream.sol() ? tlvFirstLevelIndentMatch : /^   /,
            ))
          ) {
            // Indentation
            style = // make this style distinct from the previous one to prevent
              // codemirror from combining spans
              'tlv-indent-' +
              (stream.pos % 2 == 0 ? 'even' : 'odd') +
              // and style it
              ' ' +
              tlvScopeStyle(state, stream.pos - tlvIndentUnit, 'indent');
            // Style the line prefix character.
            if (match[0].charAt(0) == '!') {
              style += ' tlv-alert-line-prefix';
            }
            // Place a class before a scope identifier.
            if (tlvIdentNext(stream)) {
              style +=
                ' ' + tlvScopeStyle(state, stream.pos, 'before-scope-ident');
            }
          } else if (state.tlvInBlockComment) {
            // In a block comment.
            if (stream.match(/^.*?\*\//)) {
              // Exit block comment.
              state.tlvInBlockComment = false;
              if (tlvTrackStatements && !stream.eol()) {
                // Anything after comment is assumed to be real statement content.
                state.statementComment = false;
              }
            } else {
              stream.skipToEnd();
            }
            style = 'comment';
          } else if (
            (match = stream.match(tlvCommentMatch)) &&
            !state.tlvInBlockComment
          ) {
            // Start comment.
            if (match[0] == '//') {
              // Line comment.
              stream.skipToEnd();
            } else {
              // Block comment.
              state.tlvInBlockComment = true;
            }
            style = 'comment';
          } else if ((match = stream.match(tlvIdentMatch))) {
            // looks like an identifier (or identifier prefix)
            var prefix = match[1];
            var mnemonic = match[2];
            if (
              // is identifier prefix
              tlvIdentifierStyle.hasOwnProperty(prefix) &&
              // has mnemonic or we're at the end of the line (maybe it hasn't been typed yet)
              (mnemonic.length > 0 || stream.eol())
            ) {
              style = tlvIdentifierStyle[prefix];
              if (stream.column() == state.indented) {
                // Begin scope.
                style +=
                  ' ' + tlvScopeStyle(state, stream.column(), 'scope-ident');
              }
            } else {
              // Just swallow one character and try again.
              // This enables subsequent identifier match with preceding symbol character, which
              //   is legal within a statement.  (Eg, !$reset).  It also enables detection of
              //   comment start with preceding symbols.
              stream.backUp(stream.current().length - 1);
              style = 'tlv-default';
            }
          } else if (stream.match(/^\t+/)) {
            // Highlight tabs, which are illegal.
            style = 'tlv-tab';
          } else if (stream.match(/^[\[\]{}\(\);\:]+/)) {
            // [:], (), {}, ;.
            style = 'meta';
          } else if ((match = stream.match(/^[mM]4([\+_])?[\w\d_]*/))) {
            // m4 pre proc
            style = match[1] == '+' ? 'tlv-m4-plus' : 'tlv-m4';
          } else if (stream.match(/^ +/)) {
            // Skip over spaces.
            if (stream.eol()) {
              // Trailing spaces.
              style = 'error';
            } else {
              // Non-trailing spaces.
              style = 'tlv-default';
            }
          } else if (stream.match(/^[\w\d_]+/)) {
            // alpha-numeric token.
            style = 'number';
          } else {
            // Eat the next char w/ no formatting.
            stream.next();
            style = 'tlv-default';
          }
          if (beginStatement) {
            style += ' tlv-statement';
          }
        } else {
          if (stream.match(/^[mM]4([\w\d_]*)/)) {
            // m4 pre proc
            style = 'tlv-m4';
          }
        }
        return style;
      },

      indent: function(state) {
        return state.tlvCodeActive == true ? state.tlvNextIndent : -1;
      },

      startState: function(state) {
        state.tlvIndentationStyle = []; // Styles to use for each level of indentation.
        state.tlvCodeActive = true; // True when we're in a TLV region (and at beginning of file).
        state.tlvNextIndent = -1; // The number of spaces to autoindent the next line if tlvCodeActive.
        state.tlvInBlockComment = false; // True inside /**/ comment.
        if (tlvTrackStatements) {
          state.statementComment = false; // True inside a statement's header comment.
        }
      },
    },
  });
});
